require_relative './ast.rb'

module Stone
  class BasicEnv
    def initialize
      @env = { }
    end

    def get(name)
      @env[name]
    end

    def put(name, val)
      @env[name] = val
    end
  end

  class NestedEnv
    attr_writer :outer

    # todo 
    # may every Env has an @outer env
    # called Kernel like ruby does
    def initialize(env)
      @env   = { }
      @outer = env
    end

    def get(name)
      val = @env[name]
      return val if val
      return val if !@outer
      
      @outer.get name
    end

    def put(name, val)
      where(name).instance_eval do 
        @env[name] = val
      end
    end

    def where(name)
      return self if @env.has_key?(name)
      return self if !@outer

      @outer.where name
    end
  end

  class NumberLiteray
    def evaluate(_)
      token.value
    end
  end

  class Name
    def evaluate(env)
      env.get token.value
    end
  end

  class StringLiteray
    def evaluate(_)
      token.value
    end
  end

  class NegExpr 
    def evaluate(env)
      -1 * oprand.evaluate(env)
    end
  end

  class PrimaryExpr
    def evaluate(env)
      eval_subexpr env, 0
    end

    def eval_subexpr(env, nest)
      if postfix?(nest)
        tar = eval_subexpr(env, nest+1)
        postfix(nest).evaluate env, tar
      else
        oprand.evaluate env
      end
    end
  end

  class BinaryExpr
    def evaluate(env)
      if op == "="
        r = right.evaluate(env)
        compute_assign env, r
      else
        l = left.evaluate(env)
        r = right.evaluate(env)

        compute_op(op, l, r)
      end
    end

    def compute_assign(env, r)
      l = left
      # todo exception handing 
      env.put l.value, r
      r
    end
    
    # oprations behaviour same as ruby
    def compute_op(op, l, r)
      # todo exception handing 
      l.send op, r
    end
  end

  class NullStmnt
    def evaluate(_)
      # todo chose a better ret val
      "null statement"
    end
  end

  class BlockStmnt
    def evaluate(env)
      # what should return by default? 
      children
        .inject(0) { |ret, child| child.evaluate env }

    end
  end

  # maybe we can suppot :
  # var = if true { 1 } else { 0 }
  class IfStmnt
    def evaluate(env)
      # Stone only handle true or false 
      # as the condition branch
      case cond.evaluate(env)
      when true
        then_block.evaluate env
      when false
        return 0 if !else_block

        else_block.evaluate env
      else
        # exception handing 
      end
    end
  end

  class WhileStmnt
    def evaluate(env)
      ret = 0
      loop do 
        case cond.evaluate(env)
        when true
          ret = block.evaluate(env)
        when false
          break 
        else
          # exception handing 
        end
      end

      ret
    end
  end

  class Function < Struct.new(:params, :body, :env)
    def make_env
      NestedEnv.new env
    end

    def to_s
      "<fn: #{000}>"
    end
  end

  class FnStmnt
    def evaluate(env)
      env.put name, Function.new(params, body, env)
      name
    end
  end

  class ParameterList
    def evaluate(env, idx, val)
      n = name(idx)
      env.instance_eval { @env[n] = val }
    end
  end

  class Arguments 
    def evaluate(caller_env, val)
      if !val.instance_of?(Function)
        # todo exception handing 
        puts "val is not a instance of Function"
      end

      params = val.params
      if num != params.size
        # todo exception handing
        puts "error arguements size"
      end

      nenv = val.make_env
      children.each_with_index do |c, idx|
        params.evaluate nenv, idx, c.evaluate(caller_env)
      end

      val.body.evaluate nenv
    end
  end
end
